Oggi faremo il progetto finale sul quarto modulo JRU. Cosa sarà? Proviamo a lavorare con diverse tecnologie: MySQL, Hibernate, Redis, Docker. Ora più soggetto.

Compito: abbiamo un database MySQL relazionale con uno schema (paese-città, lingua per paese). E c'è una richiesta frequente della città, che rallenta. Abbiamo trovato una soluzione: spostare tutti i dati richiesti frequentemente su Redis (nella memoria di tipo valore-chiave).

E non abbiamo bisogno di tutti i dati archiviati in MySQL, ma solo di un insieme selezionato di campi. Il progetto avrà la forma di un tutorial. Cioè, qui solleveremo il problema e lo risolveremo immediatamente.

Quindi, iniziamo con quale software avremo bisogno:

  1. IDEA Ultimate (che ha esaurito la chiave - scrivi a Roman in the Slack)
  2. Workbench (o qualsiasi altro client per MySQL)
  3. Docker
  4. redis-insight - facoltativo

Il nostro piano d'azione:

  1. Configura la finestra mobile (non lo farò nel tutorial, perché ogni sistema operativo avrà le sue caratteristiche e ci sono molte risposte su Internet a domande come "come installare la finestra mobile su Windows"), controlla che tutto funzioni.
  2. Esegui il server MySQL come contenitore docker.
  3. Espandi dump .
  4. Crea un progetto in Idea, aggiungi dipendenze maven.
  5. Crea dominio di livello.
  6. Scrivi un metodo per ottenere tutti i dati da MySQL.
  7. Scrivi un metodo di trasformazione dei dati (in Redis scriveremo solo i dati richiesti frequentemente).
  8. Esegui il server Redis come contenitore docker.
  9. Scrivi i dati su Redis.
  10. Facoltativo: installa redis-insight, guarda i dati archiviati in Redis.
  11. Scrivi un metodo per ottenere dati da Redis.
  12. Scrivi un metodo per ottenere dati da MySQL.
  13. Confronta la velocità con cui ottieni gli stessi dati da MySQL e Redis.

Configurazione della finestra mobile

Docker è una piattaforma aperta per lo sviluppo, la consegna e il funzionamento delle applicazioni. Lo useremo per non installare e configurare Redis sulla macchina locale, ma per utilizzare un'immagine già pronta. Puoi leggere di più su docker qui o vederlo qui . Se non hai familiarità con la finestra mobile, ti consiglio di guardare solo il secondo link.

Per assicurarti di avere docker installato e configurato, esegui il comando:docker -v

Se tutto è OK, vedrai la versione docker

Esegui il server MySQL come contenitore docker

Per poter confrontare il tempo di restituzione dei dati da MySQL e Redis, utilizzeremo anche MySQL nella finestra mobile. In PowerShell (o in un altro terminale della console se non utilizzi Windows), esegui il comando:

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

Considera cosa stiamo facendo con questo comando:

  • docker run– lanciare (e scaricare, se non è ancora stato scaricato sulla macchina locale) l'immagine. Come risultato del lancio, otteniamo un contenitore in esecuzione.
  • --name mysql- impostare il nome del contenitore mysql.
  • -d- un flag che dice che il contenitore dovrebbe continuare a funzionare, anche se chiudi la finestra del terminale da cui è stato avviato questo contenitore.
  • -p 3306:3306- specifica le porte. Prima dei due punti - la porta sulla macchina locale, dopo i due punti - la porta nel contenitore.
  • -e MYSQL_ROOT_PASSWORD=root– passando la variabile d'ambiente MYSQL_ROOT_PASSWORD con il valore root al contenitore. Flag specifico per l'immagine mysql/
  • --restart unless-stopped- impostazione della politica di comportamento (se il contenitore deve essere riavviato alla chiusura). Il valore a meno che non venga arrestato significa riavviare sempre, tranne quando il contenitore è stato arrestato /
  • -v mysql:/var/lib/mysql – aggiungere volume (immagine per la memorizzazione delle informazioni).
  • mysql:8 – il nome dell'immagine e la sua versione.

Dopo aver eseguito il comando nel terminale, la finestra mobile scaricherà tutti i livelli dell'immagine e avvierà il contenitore:

Nota importante: se MySQL è installato come servizio sul computer locale ed è in esecuzione, è necessario specificare una porta diversa nel comando di avvio o arrestare questo servizio in esecuzione.

Espandi dump

Per espandere il dump, è necessario creare una nuova connessione al database da Workbench, dove si specificano i parametri. Ho usato la porta predefinita (3306), non ho cambiato il nome utente (root per impostazione predefinita) e ho impostato la password per l'utente root (root).

In Workbench, fai Data Import/Restoree seleziona Import from Self Contained File. Specifica dove hai scaricato il dump come file . Non è necessario creare lo schema in anticipo: la sua creazione è inclusa nel file dump. Dopo un'importazione riuscita, avrai uno schema mondiale con tre tabelle:

  1. city ​​è una tabella di città.
  2. paese - tavolo paese.
  3. country_language - una tabella che indica quale percentuale della popolazione nel paese parla una determinata lingua.

Poiché abbiamo utilizzato un volume all'avvio del contenitore, dopo aver arrestato e persino eliminato il contenitore mysql e rieseguito il comando start ( docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8), non sarà necessario distribuire nuovamente il dump: è già distribuito nel volume.

Crea un progetto in Idea, aggiungi dipendenze maven

Sai già come creare un progetto nell'Idea: questo è il punto più semplice del progetto di oggi.

Aggiungi dipendenze al file 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> 

Le prime tre dipendenze ti sono familiari da tempo.

lettuce-coreè uno dei client Java disponibili per lavorare con Redis.

jackson-databind– dipendenza per l'utilizzo di ObjectMapper (per trasformare i dati per l'archiviazione in Redis (valore-chiave di tipo String)).

Anche nella cartella delle risorse (src/main/resources) aggiungi spy.properties per visualizzare le richieste con i parametri che Hibernate esegue. Contenuto dell'archivio:

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 

Crea dominio di livello

Crea il pacchetto com.codegym.domain

Per me è conveniente quando si mappano le tabelle su un'entità per utilizzare la struttura della tabella nell'Idea, quindi aggiungiamo una connessione al database nell'Idea.

Suggerisco di creare entità in questo ordine:

  • Paese
  • Città
  • PaeseLingua

È auspicabile che tu esegua tu stesso la mappatura.

Codice classe paese:

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

}

Ci sono 3 punti interessanti nel codice.

Il primo è Continent enam , memorizzato nel database come valori ordinali. Nella struttura della tabella paese, nei commenti al campo continente, puoi vedere quale valore numerico corrisponde a quale continente.

package com.codegym.domain;

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

Il secondo punto è un insieme di entitàCountryLanguage . Ecco un link @OneToManyche non era nella seconda bozza di questo modulo. Per impostazione predefinita, Hibernate non estrarrà il valore di questo set quando richiede un'entità paese. Ma poiché abbiamo bisogno di sottrarre tutti i valori dal database relazionale per la memorizzazione nella cache, il file FetchType.EAGER.

Il terzo è il campo della città . Comunicazione @OneToOne- come se tutto fosse familiare e comprensibile. Ma, se osserviamo la struttura della chiave esterna nel database, vediamo che il paese (paese) ha un collegamento alla capitale (città) e la città (città) ha un collegamento al paese (paese). C'è una relazione ciclica.

Non faremo ancora nulla con questo, ma quando arriviamo all'elemento "Scrivi un metodo per ottenere tutti i dati da MySQL", vediamo quali query esegue Hibernate, guardiamo il loro numero e ricordiamo questo elemento.

Codice classe città:

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

}

Codice classe 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
}

Scrivi un metodo per ottenere tutti i dati da MySQL

Nella classe Main dichiariamo i campi:

private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

e inizializzarli nel costruttore della classe Main:

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

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

Come puoi vedere, non ci sono abbastanza metodi e classi: scriviamoli.

Dichiara un pacchetto com.codegym.dao e aggiungi 2 classi ad esso:

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

Ora puoi importare queste 2 classi in Main. Mancano ancora due metodi:

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

Non abbiamo ancora raggiunto il ravanello, quindi l'implementazione dell'inizializzazione del client ravanello rimarrà per ora uno stub:

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

Infine, possiamo scrivere un metodo in cui estraiamo tutte le città:

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 caratteristica di implementazione è tale che otteniamo 500 città ciascuna. Questo è necessario perché ci sono restrizioni sulla quantità di dati trasmessi. Sì, nel nostro caso, non li raggiungeremo, perché. abbiamo un totale di 4079 città nel database. Ma nelle applicazioni di produzione, quando è necessario ottenere molti dati, questa tecnica viene spesso utilizzata.

E l'implementazione del metodo principale:

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

Ora possiamo eseguire la nostra applicazione in debug per la prima volta e vedere come funziona (o non funziona - sì, succede spesso).

Le città stanno diventando. Ogni città ottiene un paese, se non è stato precedentemente sottratto dal database di un'altra città. Calcoliamo approssimativamente quante query Hibernate invierà al database:

  • 1 richiesta per scoprire il numero totale di città (necessario per iterare oltre 500 città per sapere quando fermarsi).
  • 4079 / 500 = 9 richieste (lista città).
  • Ogni città ottiene un paese, se non è stato sottratto in precedenza. Poiché ci sono 239 paesi nel database, questo ci darà 239 query.

Total 249 requests. E abbiamo anche detto che insieme al Paese avremmo ricevuto immediatamente una serie di lingue, altrimenti ci sarebbe stata l'oscurità in generale. Ma è ancora molto, quindi modifichiamo un po' il comportamento. Partiamo dalle riflessioni: cosa fare, dove correre? Ma seriamente, perché ci sono così tante richieste. Se guardi il registro delle richieste, vediamo che ogni paese è richiesto separatamente, quindi la prima semplice soluzione: richiediamo tutti i paesi insieme, perché sappiamo in anticipo che avremo bisogno di tutti in questa transazione.

Nel metodo fetchData(), subito dopo l'inizio della transazione, aggiungi la seguente riga:

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

Contiamo le richieste:

  • 1 - ottieni tutti i paesi
  • 239 - query per ogni paese della sua capitale
  • 1 - richiesta del numero di città
  • 9 - richiesta elenchi di città

Total 250. L'idea è buona, ma non ha funzionato. Il problema è che il paese ha un collegamento con la capitale (città) @OneToOne. E tale collegamento viene caricato immediatamente per impostazione predefinita ( FetchType.EAGER). Mettiamo FetchType.LAZY, perché in ogni caso, caricheremo tutte le città successivamente nella stessa transazione.

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

I capitali non vengono più richiesti separatamente, ma il numero delle richieste non è cambiato. Ora, per ogni paese, l'elenco CountryLanguage viene richiesto da una query separata . Cioè, ci sono progressi e ci stiamo muovendo nella giusta direzione. Se ricordi, le lezioni hanno suggerito la soluzione "join fetch" per richiedere un'entità con dati dipendenti in una richiesta aggiungendo un ulteriore join alla richiesta. In CountryDAO , riscrivi la query HQL nel metodo getAll()per:

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

Lancio. Guardiamo il registro, contiamo le richieste:

  • 1 - tutti i paesi con lingue
  • 1 - numero di città
  • 9 - elenchi di città.

Total 11- ci siamo riusciti)) Se non solo hai letto tutto questo testo, ma hai anche provato a eseguirlo dopo ogni fase di messa a punto dell'applicazione, dovresti anche notare visivamente più volte l'accelerazione dell'intera applicazione.

Scrivere un metodo di trasformazione dei dati

Creiamo un pacchetto com.codegym.redisin cui aggiungiamo 2 classi: CityCountry (dati sulla città e il paese in cui si trova questa città) e Language (dati sulla lingua). Ecco tutti i campi che spesso vengono richiesti “per mansione” nella “richiesta di frenatura”.

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
}

Nel metodo principale, dopo aver ottenuto tutte le città, aggiungi la riga

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

E implementa questo metodo:

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

Penso che questo metodo sia autoesplicativo: creiamo semplicemente un'entità CityCountry e la riempiamo con i dati di City , Country , CountryLanguage .

Esegui il server Redis come contenitore docker

Ci sono 2 opzioni qui. Se esegui il passaggio facoltativo "installa redis-insight, guarda i dati archiviati in Redis", il comando è per te:

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

Se decidi di saltare questo passaggio, allora semplicemente:

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

La differenza è che nella prima opzione, la porta 8001 viene inoltrata alla macchina locale, alla quale è possibile connettersi con un client esterno per vedere cosa è memorizzato all'interno. E davo nomi significativi, quindi, redis-stacko redis.

Dopo l'avvio, puoi vedere l'elenco dei container in esecuzione. Per fare ciò, esegui il comando:

docker container ls 

E vedrai qualcosa del genere:

Se hai bisogno di trovare qualche comando, puoi guardare la guida nel terminale (docker help) o google "how to ..." (ad esempio, docker how to remove running container).

E abbiamo anche chiamato l'inizializzazione del client ravanello nel costruttore principale, ma non abbiamo implementato il metodo stesso. Aggiungi implementazione:

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 è stato aggiunto per scopi didattici in modo che nel registro di avvio sia possibile vedere che tutto è OK e la connessione tramite il client ravanello è passata senza errori.

Scrivi i dati su Redis

Aggiungi una chiamata al metodo principale

main.pushToRedis(preparedData); 

Con questa implementazione del metodo:

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

    }
}

Qui viene aperta una connessione sincrona con il client ravanello e in sequenza ogni oggetto del tipo CityCountry viene scritto nel ravanello. Poiché il ravanello è un String key-value store , la chiave (city id) viene convertita in una stringa. E il valore è anche per la stringa, ma utilizzando ObjectMapper in formato JSON.

Resta da eseguire e verificare che non ci siano errori nel registro. Tutto ha funzionato.

Installa redis-insight, guarda i dati archiviati in Redis (facoltativo)

Scarica redis-insight dal link e installalo. Dopo l'avvio, mostra immediatamente la nostra istanza ravanello nel contenitore docker:

Se accedi, vedremo un elenco di tutte le chiavi:

E puoi andare su qualsiasi chiave per vedere quali dati sono memorizzati su di essa:

Scrivi un metodo per ottenere dati da Redis

Per il test, utilizziamo il seguente test: otteniamo 10 record CityCountry. Ciascuno con una richiesta separata, ma in una connessione.

I dati dal ravanello possono essere ottenuti tramite il nostro client ravanello. Per fare ciò, scriviamo un metodo che richiede un elenco di id da ottenere.

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

L'implementazione, credo, è intuitiva: apriamo una connessione sincrona e per ogni id otteniamo una JSON String , che convertiamo nell'oggetto del tipo CityCountry di cui abbiamo bisogno .

Scrivi un metodo per ottenere dati da MySQL

Nella classe CityDAO , aggiungi un metodo getById(Integer id)in cui otterremo la città insieme al paese:

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

Per analogia con il paragrafo precedente, aggiungiamo un metodo simile per MySQL alla classe Main:

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

Delle funzionalità, per essere sicuri di ottenere l'oggetto completo (senza stub proxy), richiediamo esplicitamente un elenco di lingue dal paese.

Confronta la velocità con cui ottieni gli stessi dati da MySQL e Redis

Qui fornirò immediatamente il codice del metodo principale e il risultato che si ottiene sul mio computer locale.

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

Durante il test, è presente una funzionalità: i dati di MySQL vengono solo letti, quindi non possono essere riavviati tra l'avvio della nostra applicazione. E in Redis sono scritti.

Sebbene quando provi ad aggiungere un duplicato per la stessa chiave, i dati verranno semplicemente aggiornati, ti consiglio di eseguire i comandi per arrestare il contenitore docker stop redis-stacked eliminare il contenitore tra l'avvio dell'applicazione nel terminale docker rm redis-stack. Successivamente, solleva nuovamente il contenitore con il ravanello docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:lateste solo dopo esegui la nostra applicazione.

Ecco i risultati del mio test:

In totale, abbiamo ottenuto un aumento di una volta e mezza delle prestazioni della risposta alla richiesta di "frenata frequente". E questo tiene conto del fatto che nei test non abbiamo utilizzato la deserializzazione più veloce tramite ObjectMapper . Se lo cambi in GSON, molto probabilmente, puoi "guadagnare" un po' più di tempo.

In questo momento, ricordo una barzelletta su un programmatore e il tempo: leggi e pensa a come scrivere e ottimizzare il tuo codice.