Hoy haremos el proyecto final sobre el cuarto módulo JRU. ¿Qué será? Intentemos trabajar con diferentes tecnologías: MySQL, Hibernate, Redis, Docker. Ahora más tema.

Tarea: tenemos una base de datos MySQL relacional con un esquema (país-ciudad, idioma por país). Y hay una petición frecuente de la ciudad, que frena. Se nos ocurrió una solución: mover todos los datos que se solicitan con frecuencia a Redis (en almacenamiento de memoria del tipo clave-valor).

Y no necesitamos todos los datos almacenados en MySQL, sino solo un conjunto seleccionado de campos. El proyecto tendrá la forma de un tutorial. Es decir, aquí plantearemos el problema y lo resolveremos de inmediato.

Entonces, comencemos con qué software necesitaremos:

  1. IDEA Ultimate (a quién se le acabó la clave; escriba a Roman en Slack)
  2. Workbench (o cualquier otro cliente para MySQL)
  3. Estibador
  4. redis-insight - opcional

Nuestro plan de acción:

  1. Configure docker (no lo haré en el tutorial, porque cada sistema operativo tendrá sus propias características y hay muchas respuestas en Internet a preguntas como "cómo instalar docker en Windows"), verifique que todo funcione.
  2. Ejecute el servidor MySQL como un contenedor docker.
  3. Ampliar volcado .
  4. Cree un proyecto en Idea, agregue dependencias maven.
  5. Hacer dominio de capa.
  6. Escriba un método para obtener todos los datos de MySQL.
  7. Escriba un método de transformación de datos (en Redis escribiremos solo los datos que se solicitan con frecuencia).
  8. Ejecute el servidor Redis como un contenedor docker.
  9. Escribir datos en Redis.
  10. Opcional: instale redis-insight, mire los datos almacenados en Redis.
  11. Escriba un método para obtener datos de Redis.
  12. Escriba un método para obtener datos de MySQL.
  13. Compare la velocidad de obtener los mismos datos de MySQL y Redis.

Configuración de la ventana acoplable

Docker es una plataforma abierta para desarrollar, entregar y operar aplicaciones. Lo usaremos para no instalar y configurar Redis en la máquina local, sino para usar una imagen preparada. Puede leer más sobre Docker aquí o verlo aquí . Si no está familiarizado con Docker, le recomiendo mirar solo el segundo enlace.

Para asegurarse de que tiene Docker instalado y configurado, ejecute el comando:docker -v

Si todo está bien, verá la versión de la ventana acoplable

Ejecute el servidor MySQL como contenedor docker

Para poder comparar el tiempo de devolución de datos de MySQL y Redis, también usaremos MySQL en la ventana acoplable. En PowerShell (u otra terminal de consola si no usa Windows), ejecute el comando:

docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8 

Considere lo que estamos haciendo con este comando:

  • docker run– lanzar (y descargar, si aún no se ha descargado en la máquina local) la imagen. Como resultado del lanzamiento, obtenemos un contenedor en ejecución.
  • --name mysql- establecer el nombre del contenedor mysql.
  • -d- una bandera que dice que el contenedor debería continuar funcionando, incluso si cierra la ventana de la terminal desde la que se inició este contenedor.
  • -p 3306:3306- especifica los puertos. Antes de los dos puntos, el puerto en la máquina local, después de los dos puntos, el puerto en el contenedor.
  • -e MYSQL_ROOT_PASSWORD=root– pasando la variable de entorno MYSQL_ROOT_PASSWORD con el valor raíz al contenedor. Indicador específico de la imagen mysql/
  • --restart unless-stopped- establecer la política de comportamiento (si el contenedor debe reiniciarse cuando está cerrado). El valor a menos que se detenga significa reiniciar siempre, excepto cuando el contenedor se detuvo /
  • -v mysql:/var/lib/mysql – agregar volumen (imagen para almacenar información).
  • mysql:8 – el nombre de la imagen y su versión.

Después de ejecutar el comando en la terminal, la ventana acoplable descargará todas las capas de la imagen e iniciará el contenedor:

Nota importante: si tiene MySQL instalado como un servicio en su computadora local y se está ejecutando, debe especificar un puerto diferente en el comando de inicio o detener este servicio en ejecución.

Ampliar volcado

Para expandir el volcado, debe crear una nueva conexión a la base de datos desde Workbench, donde especifica los parámetros. Usé el puerto predeterminado (3306), no cambié el nombre de usuario (raíz por defecto) y configuré la contraseña para el usuario raíz (raíz).

En Workbench, haga Data Import/Restorey seleccione Import from Self Contained File. Especifique dónde descargó el volcado como un archivo . No necesita crear el esquema de antemano; su creación se incluye en el archivo de volcado. Después de una importación exitosa, tendrá un esquema mundial con tres tablas:

  1. city ​​es una tabla de ciudades.
  2. país - tabla de países.
  3. country_language: una tabla que indica qué porcentaje de la población del país habla un idioma en particular.

Dado que usamos un volumen al iniciar el contenedor, después de detener e incluso eliminar el contenedor mysql y volver a ejecutar el comando de inicio ( docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8), no habrá necesidad de implementar el volcado nuevamente; ya está implementado en el volumen.

Crear proyecto en Idea, agregar dependencias maven

Ya sabe cómo crear un proyecto en la Idea: este es el punto más fácil del proyecto de hoy.

Agregue dependencias al archivo 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> 

Las tres primeras dependencias te resultan familiares desde hace mucho tiempo.

lettuce-corees uno de los clientes Java disponibles para trabajar con Redis.

jackson-databind– dependencia para usar ObjectMapper (para transformar datos para almacenamiento en Redis (clave-valor de tipo String)).

También en la carpeta de recursos (src/main/resources) agregue spy.properties para ver las solicitudes con parámetros que ejecuta Hibernate. Contenido del archivo:

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 

Hacer dominio de capa

Crear paquete com.codegym.domain

Cuando mapeo tablas en una entidad, me resulta conveniente usar la estructura de la tabla en la Idea, así que agreguemos una conexión de base de datos en la Idea.

Sugiero crear entidades en este orden:

  • País
  • ciudad
  • Lenguaje del pais

Es deseable que realice el mapeo usted mismo.

Código de clase de país:

package com.codegym.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

}

Hay 3 puntos interesantes en el código.

El primero es Continent enam , que se almacena en la base de datos como valores ordinales. En la estructura de la tabla de países, en el campo de comentarios al continente, se puede ver qué valor numérico corresponde a qué continente.

package com.codegym.domain;

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

El segundo punto es un conjunto de entidadesCountryLanguage . Aquí hay un enlace @OneToManyque no estaba en el segundo borrador de este módulo. De forma predeterminada, Hibernate no extraerá el valor de este conjunto cuando solicite una entidad de país. Pero como necesitamos restar todos los valores de la base de datos relacional para el almacenamiento en caché, el archivo FetchType.EAGER.

El tercero es el campo de la ciudad . Comunicación @OneToOne: como todo, es familiar y comprensible. Pero, si observamos la estructura de la clave externa en la base de datos, vemos que el país (country) tiene un vínculo con la capital (ciudad) y la ciudad (city) tiene un vínculo con el país (country). Hay una relación cíclica.

No haremos nada con esto todavía, pero cuando lleguemos al elemento "Escribir un método para obtener todos los datos de MySQL", veamos qué consultas ejecuta Hibernate, veamos su número y recordemos este elemento.

Código de clase de ciudad:

package com.codegym.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

}

Código de clase CountryLanguage:

package com.codegym.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
}

Escriba un método para obtener todos los datos de MySQL

En la clase Main, declaramos los campos:

private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

e inicialícelos en el constructor de la clase Main:

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

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

Como puede ver, no hay suficientes métodos y clases, escribámoslos.

Declare un paquete com.codegym.dao y agréguele 2 clases:

package com.codegym.dao;

import com.codegym.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.codegym.dao;

import com.codegym.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());
    }
}

Ahora puede importar estas 2 clases a Main. Todavía faltan dos métodos:

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;
}

Todavía no hemos llegado al rábano, por lo que la implementación de la inicialización del cliente rábano seguirá siendo un stub por ahora:

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

Finalmente, podemos escribir un método en el que extraigamos todas las ciudades:

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;
    }
}

La característica de implementación es tal que obtenemos 500 ciudades cada una. Esto es necesario porque existen restricciones en la cantidad de datos transmitidos. Sí, en nuestro caso, no llegaremos a ellos, porque. tenemos un total de 4079 ciudades en la base de datos. Pero en aplicaciones de producción, cuando necesita obtener una gran cantidad de datos, esta técnica se usa a menudo.

Y la implementación del método principal:

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

Ahora podemos ejecutar nuestra aplicación en depuración por primera vez y ver cómo funciona (o no funciona, sí, sucede a menudo).

Las ciudades están recibiendo. Cada ciudad obtiene un país, si no se ha sustraído previamente de la base de datos para otra ciudad. Calculemos aproximadamente cuántas consultas enviará Hibernate a la base de datos:

  • 1 solicitud para averiguar el número total de ciudades (necesita iterar más de 500 ciudades para saber cuándo detenerse).
  • 4079 / 500 = 9 solicitudes (lista de ciudades).
  • Cada ciudad obtiene un país, si no se ha restado antes. Dado que hay 239 países en la base de datos, esto nos dará 239 consultas.

Total 249 requests. Y también dijimos que junto con el país deberíamos recibir de inmediato un conjunto de idiomas, de lo contrario, habría oscuridad en general. Pero todavía es mucho, así que modifiquemos un poco el comportamiento. Comencemos con las reflexiones: ¿qué hacer, por dónde correr? Pero en serio, ¿por qué hay tantas solicitudes? Si observa el registro de solicitudes, vemos que cada país se solicita por separado, por lo que la primera solución simple: solicitemos todos los países juntos, porque sabemos de antemano que los necesitaremos a todos en esta transacción.

En el método fetchData(), inmediatamente después del inicio de la transacción, agregue la siguiente línea:

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

Contamos solicitudes:

  • 1 - obtener todos los países
  • 239 - consulta por cada pais de su capital
  • 1 - solicitud del número de ciudades
  • 9 - solicitud de listas de ciudades

Total 250. La idea es buena, pero no funcionó. El problema es que el país tiene conexión con la capital (ciudad) @OneToOne. Y dicho enlace se carga inmediatamente de forma predeterminada ( FetchType.EAGER). Pongamos FetchType.LAZY, porque de todos modos, cargaremos todas las ciudades más tarde en la misma transacción.

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

Ya no se piden capitales por separado, pero el número de solicitudes no ha variado. Ahora, para cada país, la lista CountryLanguage se solicita mediante una consulta separada . Es decir, hay progreso y vamos en la dirección correcta. Si recuerda, las conferencias sugirieron la solución de "búsqueda conjunta" para solicitar una entidad con datos dependientes en una solicitud agregando una unión adicional a la solicitud. En CountryDAO , reescriba la consulta HQL en el método getAll()para:

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

Lanzamiento. Miramos el registro, contamos las solicitudes:

  • 1 - todos los países con idiomas
  • 1 - número de ciudades
  • 9 - listas de ciudades.

Total 11- lo logramos)) Si no solo leyó todo este texto, sino que también intentó ejecutarlo después de cada paso de ajuste de la aplicación, incluso debe notar visualmente la aceleración de toda la aplicación varias veces.

Escribir un método de transformación de datos

Vamos a crear un paquete com.codegym.redisen el que agreguemos 2 clases: CityCountry (datos sobre la ciudad y el país en el que se encuentra esta ciudad) e Language (datos sobre el idioma). Aquí están todos los campos que a menudo se solicitan "por tarea" en la "solicitud de frenado".

package com.codegym.redis;

import com.codegym.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.codegym.redis;

import java.math.BigDecimal;

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

    //Getters and Setters omitted
}

En el método principal, después de obtener todas las ciudades, agregue la línea

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

E implementar este método:

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());
}

Creo que este método se explica por sí mismo: simplemente creamos una entidad CityCountry y la llenamos con datos de City , Country , CountryLanguage .

Ejecute el servidor Redis como un contenedor docker

Hay 2 opciones aquí. Si realiza el paso opcional "instalar redis-insight, ver los datos almacenados en Redis", entonces el comando es para usted:

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

Si decide omitir este paso, simplemente:

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

La diferencia es que en la primera opción, el puerto 8001 se reenvía a la máquina local, a la que puedes conectarte con un cliente externo para ver qué hay almacenado en su interior. Y solía dar nombres significativos, por lo tanto, redis-stacko redis.

Después del lanzamiento, puede ver la lista de contenedores en ejecución. Para hacer esto, ejecute el comando:

docker container ls 

Y verás algo como esto:

Si necesita encontrar algún comando, puede consultar la ayuda de la terminal (ayuda de la ventana acoplable) o buscar en Google "cómo..." (por ejemplo, la ventana acoplable cómo eliminar el contenedor en ejecución).

Y también llamamos a la inicialización del cliente de rábano en el constructor principal, pero no implementamos el método en sí. Agregar implementación:

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 se agregó con fines educativos para que en el registro de inicio pueda ver que todo está bien y que la conexión a través del cliente de rábano transcurrió sin errores.

Escribir datos en Redis

Agregar una llamada al método principal

main.pushToRedis(preparedData); 

Con la implementación de este método:

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();
            }
        }

    }
}

Aquí, se abre una conexión síncrona con el cliente radish y, secuencialmente, cada objeto del tipo CityCountry se escribe en el radish. Dado que el rábano es un almacén de clave-valor de cadena , la clave (id de la ciudad) se convierte en una cadena. Y el valor también es para la cadena, pero usando ObjectMapper en formato JSON.

Queda por ejecutar y comprobar que no hay errores en el registro. Todo funcionó.

Instale redis-insight, mire los datos almacenados en Redis (opcional)

Descargue redis-insight desde el enlace e instálelo. Después de comenzar, muestra inmediatamente nuestra instancia de rábano en el contenedor acoplable:

Si inicia sesión, veremos una lista de todas las claves:

Y puede ir a cualquier tecla para ver qué datos están almacenados en ella:

Escriba un método para obtener datos de Redis

Para la prueba, usamos la siguiente prueba: obtenemos 10 registros de CityCountry. Cada uno con una solicitud separada, pero en una conexión.

Los datos de rábano se pueden obtener a través de nuestro cliente de rábano. Para hacer esto, escribamos un método que tome una lista de identificaciones para obtener.

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();
            }
        }
    }
}

La implementación, creo, es intuitiva: abrimos una conexión síncrona, y para cada id obtenemos un JSON String , que convertimos en el objeto del tipo CityCountry que necesitamos .

Escriba un método para obtener datos de MySQL

En la clase CityDAO , agregue un método getById(Integer id)en el que obtendremos la ciudad junto con el país:

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();
}

Por analogía con el párrafo anterior, agreguemos un método similar para MySQL a la clase Principal:

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();
    }
}

De las características, para asegurarse de obtener el objeto completo (sin resguardos de proxy), solicitamos explícitamente una lista de idiomas del país.

Compare la velocidad de obtener los mismos datos de MySQL y Redis

Aquí daré inmediatamente el código del método principal y el resultado que se obtiene en mi computadora local.

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);

    // close the current session in order to make a query to the database for sure, and not to pull data from the cache
    main.sessionFactory.getCurrentSession().close();

    //choose random 10 id cities
    //since we did not handle invalid situations, use the existing id in the database
    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();
}

Al realizar pruebas, hay una característica: los datos de MySQL solo se leen, por lo que no se pueden reiniciar entre lanzamientos de nuestra aplicación. Y en Redis están escritos.

Aunque cuando intente agregar un duplicado para la misma clave, los datos simplemente se actualizarán, le recomendaría que ejecute los comandos para detener el contenedor docker stop redis-stacky eliminar el contenedor entre los inicios de la aplicación en la terminal docker rm redis-stack. Después de eso, levante el recipiente con el rábano nuevamente docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latesty solo después de eso ejecute nuestra aplicación.

Aquí están los resultados de mi prueba:

En total, hemos logrado aumentar en una vez y media el rendimiento de la respuesta a la solicitud de "frenado frecuente". Y esto teniendo en cuenta el hecho de que en las pruebas no usamos la deserialización más rápida a través de ObjectMapper . Si lo cambia a GSON, lo más probable es que pueda "ganar" un poco más de tiempo.

En este momento, recuerdo una broma sobre un programador y el tiempo: lee y piensa cómo escribir y optimizar tu código.