Idag ska vi göra slutprojektet på den fjärde JRU-modulen. Vad blir det? Låt oss försöka arbeta med olika tekniker: MySQL, Hibernate, Redis, Docker. Nu mer ämne.

Uppgift: vi har en relationell MySQL-databas med ett schema (land-stad, språk per land). Och det finns en frekvent begäran från staden, som saktar ner. Vi kom på en lösning - att flytta all data som efterfrågas ofta till Redis (i minneslagring av typen nyckel-värde).

Och vi behöver inte all data som är lagrad i MySQL, utan bara en utvald uppsättning fält. Projektet kommer att vara i form av en handledning. Det vill säga, här kommer vi att ta upp problemet och omedelbart lösa det.

Så låt oss börja med vilken programvara vi behöver:

  1. IDEA Ultimate (som fick slut på nyckeln - skriv till Roman in the Slack)
  2. Workbench (eller någon annan klient för MySQL)
  3. Hamnarbetare
  4. redis-insight - valfritt

Vår handlingsplan:

  1. Ställ in docker (jag kommer inte att göra detta i handledningen, eftersom varje OS kommer att ha sina egna egenskaper och det finns många svar på Internet på frågor som "hur man installerar docker på Windows"), kontrollera att allt fungerar.
  2. Kör MySQL-server som en dockningsbehållare.
  3. Expandera dump .
  4. Skapa ett projekt i Idea, lägg till maven beroenden.
  5. Gör lagerdomän.
  6. Skriv en metod för att få all data från MySQL.
  7. Skriv en datatransformationsmetod (i Redis kommer vi bara att skriva de data som efterfrågas ofta).
  8. Kör Redis-servern som en dockningsbehållare.
  9. Skriv data till Redis.
  10. Valfritt: installera redis-insight, titta på data som lagras i Redis.
  11. Skriv en metod för att få data från Redis.
  12. Skriv en metod för att hämta data från MySQL.
  13. Jämför hastigheten för att få samma data från MySQL och Redis.

Docker-inställning

Docker är en öppen plattform för att utveckla, leverera och driva applikationer. Vi kommer att använda det för att inte installera och konfigurera Redis på den lokala maskinen, utan för att använda en färdig avbildning. Du kan läsa mer om docker här eller se det här . Om du inte är bekant med docker rekommenderar jag att du bara tittar på den andra länken.

För att se till att du har docker installerat och konfigurerat, kör kommandot:docker -v

Om allt är OK kommer du att se dockerversionen

Kör MySQL-server som dockningsbehållare

För att kunna jämföra tidpunkten för att returnera data från MySQL och Redis kommer vi även att använda MySQL i dockern. I PowerShell (eller en annan konsolterminal om du inte använder Windows), kör kommandot:

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

Tänk på vad vi gör med det här kommandot:

  • docker run– starta (och ladda ner, om den ännu inte har laddats ner till den lokala maskinen) bilden. Som ett resultat av lanseringen får vi en löpande container.
  • --name mysql- ställ in namnet på mysql-behållaren.
  • -d- en flagga som säger att containern ska fortsätta att fungera, även om du stänger terminalfönstret från vilket denna container lanserades.
  • -p 3306:3306- specificerar portar. Före kolon - porten på den lokala maskinen, efter kolon - porten i behållaren.
  • -e MYSQL_ROOT_PASSWORD=root– skicka miljövariabeln MYSQL_ROOT_PASSWORD med värdet rot till behållaren. Flagga specifik för mysql/-bilden
  • --restart unless-stopped- ställa in beteendepolicyn (om behållaren ska startas om när den stängs). Värdet om inte stoppat betyder att alltid starta om, utom när behållaren stoppades /
  • -v mysql:/var/lib/mysql – lägg till volym (bild för att lagra information).
  • mysql:8 – bildens namn och dess version.

Efter att ha utfört kommandot i terminalen kommer dockeraren att ladda ner alla lager i bilden och starta behållaren:

Viktig notering: om du har MySQL installerat som en tjänst på din lokala dator och den körs, måste du ange en annan port i startkommandot, eller stoppa den här tjänsten som körs.

Expandera dump

För att expandera dumpen måste du skapa en ny anslutning till databasen från Workbench, där du anger parametrarna. Jag använde standardporten (3306), ändrade inte användarnamnet (root som standard) och ställde in lösenordet för rootanvändaren (root).

Data Import/RestoreGör och välj i Workbench Import from Self Contained File. Ange var du laddade ned dumpen som en fil . Du behöver inte skapa schemat i förväg - skapandet av det ingår i dumpfilen. Efter en lyckad import kommer du att ha ett världsschema med tre tabeller:

  1. stad är en tabell över städer.
  2. land - land tabell.
  3. country_language - en tabell som anger hur stor andel av befolkningen i landet som talar ett visst språk.

Eftersom vi använde en volym när vi startade behållaren, efter att ha stoppat och till och med tagit bort mysql-behållaren och kört startkommandot igen ( ), docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8kommer det inte att finnas något behov av att distribuera dumpen igen - den är redan distribuerad i volymen.

Skapa projekt i Idea, lägg till maven-beroenden

Du vet redan hur man skapar ett projekt i Idéen - det här är den enklaste punkten i dagens projekt.

Lägg till beroenden till pom-filen:


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

De tre första beroendena har länge varit bekanta för dig.

lettuce-coreär en av de tillgängliga Java-klienterna för att arbeta med Redis.

jackson-databind– beroende för att använda ObjectMapper (för att transformera data för lagring i Redis (nyckel-värde av typen String)).

Lägg även till spy.properties i resursmappen (src/main/resources) för att visa förfrågningarna med parametrar som Hibernate kör. Filinnehåll:

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 

Gör lagerdomän

Skapa paket com.codegym.domain

Det är bekvämt för mig när jag mappar tabeller på en enhet att använda tabellstrukturen i idén, så låt oss lägga till en databasanslutning i idén.

Jag föreslår att du skapar entiteter i denna ordning:

  • Land
  • Stad
  • CountryLanguage

Det är önskvärt att du själv utför kartläggningen.

Landsklasskod:

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

}

Det finns 3 intressanta punkter i koden.

Den första är Continent enam , som lagras i databasen som ordningsvärden. I landstabellens struktur, i kommentarerna till kontinentfältet, kan du se vilket numeriskt värde som motsvarar vilken kontinent.

package com.codegym.domain;

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

Den andra punkten är en uppsättning enheterCountryLanguage . Här är en länk @OneToManysom inte fanns i det andra utkastet av denna modul. Som standard drar Hibernate inte värdet av denna uppsättning när du begär en landsenhet. Men eftersom vi behöver subtrahera alla värden från relationsdatabasen för cachelagring, FetchType.EAGER.

Den tredje är stadsfältet . Kommunikation @OneToOne- som att allt är bekant och begripligt. Men om vi tittar på den främmande nyckelstrukturen i databasen ser vi att landet (landet) har en länk till huvudstaden (staden), och staden (staden) har en länk till landet (landet). Det finns ett cykliskt samband.

Vi kommer inte att göra något med detta ännu, men när vi kommer till "Skriv en metod för att hämta all data från MySQL", låt oss se vilka frågor Hibernate kör, titta på deras nummer och kom ihåg det här objektet. Hibernate

Stadsklasskod:

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

}

CountryLanguage klasskod:

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
}

Skriv en metod för att få all data från MySQL

I klassen Main deklarerar vi fälten:

private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

och initiera dem i konstruktorn för huvudklassen:

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

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

Som du kan se finns det inte tillräckligt med metoder och klasser - låt oss skriva dem.

Deklarera ett paket com.codegym.dao och lägg till 2 klasser till det:

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

Nu kan du importera dessa 2 klasser till Main. Fortfarande saknas två metoder:

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

Vi har ännu inte nått rädisan, så implementeringen av initieringen av rädisaklienten kommer att förbli en stubb för nu:

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

Slutligen kan vi skriva en metod där vi drar ut alla städer:

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

Implementeringsfunktionen är sådan att vi får 500 städer var. Detta är nödvändigt eftersom det finns begränsningar för mängden överförd data. Ja, i vårt fall kommer vi inte till dem, eftersom. vi har totalt 4079 städer i databasen. Men i produktionsapplikationer, när du behöver få mycket data, används denna teknik ofta.

Och genomförandet av huvudmetoden:

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

Nu kan vi köra vår applikation i debug för första gången och se hur den fungerar (eller inte fungerar - ja, det händer ofta).

Städer får. Varje stad får ett land, om det inte tidigare har subtraherats från databasen för en annan stad. Låt oss ungefär beräkna hur många frågor Hibernate kommer att skicka till databasen:

  • 1 begäran för att ta reda på det totala antalet städer (behövde iterera över 500 städer för att veta när man ska sluta).
  • 4079 / 500 = 9 förfrågningar (lista över städer).
  • Varje stad får ett land, om det inte har dragits av tidigare. Eftersom det finns 239 länder i databasen kommer detta att ge oss 239 frågor.

Total 249 requests. Och vi sa också att vi tillsammans med landet omedelbart skulle få en uppsättning språk, annars skulle det bli mörker i allmänhet. Men det är fortfarande mycket, så låt oss justera beteendet lite. Låt oss börja med reflektioner: vad ska man göra, vart ska man springa? Men seriöst – varför är det så många förfrågningar. Om du tittar på förfrågningsloggen ser vi att varje land efterfrågas separat, så den första enkla lösningen: låt oss begära alla länder tillsammans, eftersom vi vet i förväg att vi kommer att behöva dem alla i denna transaktion.

I fetchData()-metoden, omedelbart efter starten av transaktionen, lägg till följande rad:

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

Vi räknar förfrågningar:

  • 1 - få alla länder
  • 239 - fråga för varje land i dess huvudstad
  • 1 - begäran om antal städer
  • 9 - begäran om listor över städer

Total 250. Tanken är bra, men den fungerade inte. Problemet är att landet har en koppling till huvudstaden (staden) @OneToOne. Och en sådan länk laddas omedelbart som standard ( FetchType.EAGER). Låt oss sätta FetchType.LAZY, eftersom hur som helst, vi kommer att ladda alla städer senare i samma transaktion.

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

Versaler begärs inte längre separat, men antalet förfrågningar har inte ändrats. Nu, för varje land, begärs CountryLanguage- listan av en separat fråga . Det vill säga det finns framsteg, och vi går i rätt riktning. Om du kommer ihåg så föreslog föreläsningarna lösningen "join fech" för att begära en enhet med beroende data i en förfrågan genom att lägga till ytterligare en join till begäran. I CountryDAO , skriv om HQL-frågan i metoden getAll()för att:

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

Lansera. Vi tittar på loggen, räknar förfrågningar:

  • 1 - alla länder med språk
  • 1 - antal städer
  • 9 - listor över städer.

Total 11- vi lyckades)) Om du inte bara läser all den här texten, utan också försökte köra den efter varje steg av justering av applikationen, bör du till och med visuellt notera accelerationen av hela applikationen flera gånger.

Skriv en datatransformationsmetod

Låt oss skapa ett paket com.codegym.redisdär vi lägger till 2 klasser: CityCountry (data om staden och landet där denna stad ligger) och Language (data om språket). Här är alla fält som ofta efterfrågas "per uppgift" i "bromsningsförfrågan".

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
}

I huvudmetoden, efter att ha fått alla städer, lägg till raden

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

Och implementera den här metoden:

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

Jag tror att den här metoden är självförklarande: vi skapar bara en CityCountry- enhet och fyller den med data från City , Country , CountryLanguage .

Kör Redis-server som en dockningsbehållare

Det finns 2 alternativ här. Om du gör det valfria steget "installera redis-insight, titta på data som lagras i Redis", så är kommandot för dig:

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

Om du bestämmer dig för att hoppa över det här steget gör du bara:

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

Skillnaden är att i det första alternativet vidarebefordras port 8001 till den lokala maskinen, till vilken du kan ansluta med en extern klient för att se vad som finns lagrat inuti. Och jag brukade ge meningsfulla namn, därför, redis-stackeller redis.

Efter lanseringen kan du se listan över körande behållare. För att göra detta, kör kommandot:

docker container ls 

Och du kommer att se något sånt här:

Om du behöver hitta något kommando kan du antingen titta på hjälpen i terminalen (dockerhjälp) eller googla "how to ..." (till exempel docker how to remove running container).

Och vi kallade också initialiseringen av rädisklienten i huvudkonstruktören, men implementerade inte själva metoden. Lägg till implementering:

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 lades till i utbildningssyfte så att man i lanseringsloggen kan se att allt är OK och kopplingen genom radish-klienten gick utan fel.

Skriv data till Redis

Lägg till ett anrop till huvudmetoden

main.pushToRedis(preparedData); 

Med denna metodimplementering:

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

    }
}

Här öppnas en synkron anslutning med rädisaklienten och sekventiellt skrivs varje objekt av typen CityCountry till rädisan. Eftersom rädisan är en String key-value store konverteras nyckeln (city id) till en sträng. Och värdet är också till strängen, men använder ObjectMapper i JSON-format.

Det återstår att köra och kontrollera att det inte finns några fel i loggen. Allt fungerade.

Installera redis-insight, titta på data som lagras i Redis (valfritt)

Ladda ner redis-insight från länken och installera den. Efter start visar den omedelbart vår rädisa-instans i docker-behållaren:

Om du loggar in kommer vi att se en lista med alla nycklar:

Och du kan gå till valfri nyckel för att se vilken data som är lagrad på den:

Skriv en metod för att få data från Redis

För testning använder vi följande test: vi får 10 CityCountry-poster. Var och en med en separat begäran, men i en anslutning.

Data från rädisor kan erhållas genom vår rädisaklient. För att göra detta, låt oss skriva en metod som kräver en lista med id att få.

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

Implementeringen tror jag är intuitiv: vi öppnar en synkron anslutning och för varje id får vi en JSON-sträng , som vi konverterar till objektet av typen CityCountry vi behöver .

Skriv en metod för att hämta data från MySQL

I CityDAO- klassen, lägg till en metod getById(Integer id)där vi kommer att få staden tillsammans med landet:

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

I analogi med föregående stycke, låt oss lägga till en liknande metod för MySQL till Main-klassen:

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

Av funktionerna, för att vara säkra på att få hela objektet (utan proxy-stubbar), begär vi uttryckligen en lista över språk från landet.

Jämför hastigheten för att få samma data från MySQL och Redis

Här kommer jag omedelbart att ge koden för huvudmetoden, och resultatet som erhålls på min lokala dator.

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

Vid testning finns det en funktion - data från MySQL läses endast, så det kan inte startas om mellan lansering av vår applikation. Och i Redis skrivs de.

Även om när du försöker lägga till en dubblett för samma nyckel, kommer data helt enkelt att uppdateras, jag skulle rekommendera att du kör kommandona för att stoppa behållaren och ta bort behållaren docker stop redis-stackmellan programstarter i terminalen docker rm redis-stack. Efter det, höj behållaren med rädisan igen docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latestoch kör först efter det vår ansökan.

Här är mina testresultat:

Totalt har vi uppnått en ökning av prestandan för svaret på begäran om "bromsning frekvent" med en och en halv gånger. Och detta tar hänsyn till det faktum att vi i tester inte använde den snabbaste deserialiseringen genom ObjectMapper . Om du ändrar den till GSON kan du med största sannolikhet "vinna" lite mer tid.

I det här ögonblicket minns jag ett skämt om en programmerare och tid: läs och tänk på hur du skriver och optimerar din kod.