CodeGym/Curs Java/All lectures for RO purposes/Proiect înrudit: SQL, JDBC și Hibernate

Proiect înrudit: SQL, JDBC și Hibernate

Disponibil

Astăzi vom face proiectul final pe cel de-al patrulea modul JRU. Ce va fi? Să încercăm să lucrăm cu diferite tehnologii: MySQL, Hibernate, Redis, Docker. Acum mai mult subiect.

Sarcină: avem o bază de date relațională MySQL cu o schemă (țară-oraș, limbă după țară). Și există o cerere frecventă a orașului, care încetinește. Am venit cu o soluție - să mutăm toate datele care sunt solicitate frecvent în Redis (în stocarea în memorie de tipul cheie-valoare).

Și nu avem nevoie de toate datele care sunt stocate în MySQL, ci doar de un set selectat de câmpuri. Proiectul va fi sub forma unui tutorial. Adică aici vom ridica problema și o vom rezolva imediat.

Deci, să începem cu ce software vom avea nevoie:

  1. IDEA Ultimate (care a rămas fără cheie - scrie-i lui Roman in the Slack)
  2. Workbench (sau orice alt client pentru MySQL)
  3. Docher
  4. redis-insight - optional

Planul nostru de acțiune:

  1. Configurați docker (nu voi face acest lucru în tutorial, deoarece fiecare sistem de operare va avea propriile caracteristici și există o mulțime de răspunsuri pe Internet la întrebări precum „cum se instalează docker pe Windows”), verificați dacă totul funcționează.
  2. Rulați serverul MySQL ca container docker.
  3. Extindeți dump .
  4. Creați un proiect în Idea, adăugați dependențe Maven.
  5. Faceți un domeniu al stratului.
  6. Scrieți o metodă pentru a obține toate datele din MySQL.
  7. Scrieți o metodă de transformare a datelor (în Redis vom scrie doar datele care sunt solicitate frecvent).
  8. Rulați serverul Redis ca container docker.
  9. Scrieți date în Redis.
  10. Opțional: instalați redis-insight, uitați-vă la datele stocate în Redis.
  11. Scrieți o metodă pentru obținerea datelor de la Redis.
  12. Scrieți o metodă pentru obținerea datelor din MySQL.
  13. Comparați viteza de obținere a acelorași date de la MySQL și Redis.

Configurare Docker

Docker este o platformă deschisă pentru dezvoltarea, livrarea și operarea aplicațiilor. O vom folosi pentru a nu instala și configura Redis pe mașina locală, ci pentru a folosi o imagine gata făcută. Puteți citi mai multe despre docker aici sau le puteți vedea aici . Dacă nu sunteți familiarizat cu docker, vă recomand să vă uitați doar la al doilea link.

Pentru a vă asigura că aveți docker instalat și configurat, executați comanda:docker -v

Dacă totul este în regulă, veți vedea versiunea docker

Rulați serverul MySQL ca container docker

Pentru a putea compara timpul de returnare a datelor din MySQL și Redis, vom folosi și MySQL în docker. În PowerShell (sau alt terminal de consolă dacă nu utilizați Windows), rulați comanda:

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

Luați în considerare ce facem cu această comandă:

  • docker run– lansarea (și descărcarea, dacă nu a fost încă descărcată pe mașina locală) imaginea. Ca urmare a lansării, obținem un container care rulează.
  • --name mysql- setați numele containerului mysql.
  • -d- un steag care spune că containerul ar trebui să continue să funcționeze, chiar dacă închideți fereastra terminalului din care a fost lansat acest container.
  • -p 3306:3306- specifică porturile. Înainte de două puncte - portul de pe mașina locală, după două puncte - portul din container.
  • -e MYSQL_ROOT_PASSWORD=root– trecerea variabilei de mediu MYSQL_ROOT_PASSWORD cu valoarea rădăcină către container. Flag specific imaginii mysql/
  • --restart unless-stopped- stabilirea politicii de comportament (dacă containerul trebuie repornit când este închis). Valoarea unless-stop înseamnă a reporni întotdeauna, cu excepția cazului în care containerul a fost oprit /
  • -v mysql:/var/lib/mysql – adăugați volum (imagine pentru stocarea informațiilor).
  • mysql:8 – numele imaginii și versiunea acesteia.

După executarea comenzii în terminal, docker-ul va descărca toate straturile imaginii și va porni containerul:

Notă importantă: dacă aveți MySQL instalat ca serviciu pe computerul local și rulează, trebuie să specificați un alt port în comanda de pornire sau să opriți acest serviciu care rulează.

Extindeți depozitul

Pentru a extinde dump-ul, trebuie să creați o nouă conexiune la baza de date din Workbench, unde specificați parametrii. Am folosit portul implicit (3306), nu am schimbat numele de utilizator (root implicit) și am setat parola pentru utilizatorul root (root).

În Workbench, faceți Data Import/Restoreși selectați Import from Self Contained File. Specificați unde ați descărcat dump-ul ca fișier . Nu trebuie să creați schema în prealabil - crearea acesteia este inclusă în fișierul de descărcare. După un import cu succes, veți avea o schemă mondială cu trei tabele:

  1. orașul este un tabel de orașe.
  2. tara - masa de tara.
  3. country_language - un tabel care indică ce procent din populația din țară vorbește o anumită limbă.

Deoarece am folosit un volum la pornirea containerului, după oprirea și chiar ștergerea containerului mysql și reexecutarea comenzii de pornire ( docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8), nu va mai fi nevoie să implementăm din nou dump - acesta este deja implementat în volum.

Creați un proiect în Idea, adăugați dependențe Maven

Știți deja cum să creați un proiect în Idee - acesta este cel mai ușor punct din proiectul de astăzi.

Adăugați dependențe la fișierul 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> 

Primele trei dependențe îți sunt familiare de mult.

lettuce-coreeste unul dintre clienții Java disponibili pentru lucrul cu Redis.

jackson-databind– dependență pentru utilizarea ObjectMapper (pentru a transforma datele pentru stocare în Redis (cheie-valoare de tip String)).

Tot în folderul resurse (src/main/resources) adăugați spy.properties pentru a vizualiza cererile cu parametrii pe care Hibernate le execută. Conținutul fișierului:

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 

Faceți un domeniu al stratului

Creați pachetul com.codegym.domain

Este convenabil pentru mine când mapez tabele pe o entitate să folosesc structura tabelului în Idea, așa că haideți să adăugăm o conexiune la baza de date în Idea.

Vă sugerez să creați entități în această ordine:

  • Țară
  • Oraș
  • CountryLanguage

Este de dorit să efectuați singuri maparea.

Codul clasei de țară:

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

}

Există 3 puncte interesante în cod.

Primul este Continent enam , care este stocat în baza de date ca valori ordinale. În structura tabelului de țări, în comentariile la câmpul continent, puteți vedea ce valoare numerică corespunde cărui continent.

package com.codegym.domain;

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

Al doilea punct este un set de entitățiCountryLanguage . Iată un link @OneToManycare nu era în a doua versiune a acestui modul. În mod implicit, Hibernate nu va extrage valoarea acestui set atunci când solicită o entitate de țară. Dar, deoarece trebuie să scădem toate valorile din baza de date relațională pentru stocarea în cache, FetchType.EAGER.

Al treilea este câmpul orașului . Comunicare @OneToOne- ca și cum totul este familiar și de înțeles. Dar, dacă ne uităm la structura cheii străine din baza de date, vedem că țara (țara) are o legătură cu capitala (orașul), iar orașul (orașul) are o legătură către țară (țara). Există o relație ciclică.

Încă nu vom face nimic cu asta, dar când ajungem la elementul „Scrieți o metodă pentru obținerea tuturor datelor din MySQL”, să vedem ce interogări execută Hibernate, să ne uităm la numărul lor și să ne amintim acest element.

Cod clasa orasului:

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

}

Codul clasei 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
}

Scrieți o metodă pentru a obține toate datele din MySQL

În clasa Main, declarăm câmpurile:

private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

și inițializați-le în constructorul clasei Main:

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

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

După cum puteți vedea, nu există suficiente metode și clase - să le scriem.

Declarați un pachet com.codegym.dao și adăugați-i 2 clase:

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

Acum puteți importa aceste 2 clase în Main. Încă lipsesc două metode:

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

Nu am ajuns încă la ridiche, așa că implementarea inițializării clientului ridiche va rămâne un stub deocamdată:

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

În cele din urmă, putem scrie o metodă în care scoatem toate orașele:

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

Caracteristica de implementare este de așa natură încât obținem 500 de orașe fiecare. Acest lucru este necesar deoarece există restricții privind cantitatea de date transmise. Da, în cazul nostru, nu vom ajunge la ele, pentru că. avem un total de 4079 orașe în baza de date. Dar în aplicațiile de producție, când trebuie să obțineți o mulțime de date, această tehnică este adesea folosită.

Și implementarea metodei principale:

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

Acum putem rula aplicația noastră în depanare pentru prima dată și să vedem cum funcționează (sau nu funcționează - da, se întâmplă des).

Orașele devin. Fiecare oraș primește o țară, dacă aceasta nu a fost scăzută anterior din baza de date pentru alt oraș. Să calculăm aproximativ câte interogări va trimite Hibernate în baza de date:

  • 1 cerere pentru a afla numărul total de orașe (trebuie să iterați peste 500 de orașe pentru a ști când să vă opriți).
  • 4079 / 500 = 9 cereri (lista orașelor).
  • Fiecare oraș primește o țară, dacă aceasta nu a fost scăzută mai devreme. Deoarece există 239 de țări în baza de date, aceasta ne va oferi 239 de interogări.

Total 249 requests. Și am mai spus că împreună cu țara vom primi imediat un set de limbi, altfel ar fi întuneric în general. Dar este încă mult, așa că hai să modificăm puțin comportamentul. Să începem cu reflecții: ce să faci, unde să alergi? Dar serios - de ce sunt atâtea cereri. Dacă te uiți la jurnalul de cereri, vedem că fiecare țară este solicitată separat, deci prima soluție simplă: să cerem toate țările împreună, pentru că știm dinainte că vom avea nevoie de toate în această tranzacție.

În metoda fetchData(), imediat după începerea tranzacției, adăugați următoarea linie:

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

Numărăm cererile:

  • 1 - obțineți toate țările
  • 239 - interogare pentru fiecare țară din capitala sa
  • 1 - cerere pentru numărul de orașe
  • 9 - cerere de liste de orașe

Total 250. Ideea este bună, dar nu a funcționat. Problema este că țara are o legătură cu capitala (orașul) @OneToOne. Și un astfel de link este încărcat imediat în mod implicit ( FetchType.EAGER). Să punem FetchType.LAZY, pentru că oricum, vom încărca toate orașele mai târziu în aceeași tranzacție.

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

Capitalele nu mai sunt solicitate separat, dar numărul de cereri nu s-a modificat. Acum, pentru fiecare țară, lista CountryLanguage este solicitată printr-o interogare separată . Adică, există progres și ne îndreptăm în direcția bună. Dacă vă amintiți, prelegerile au sugerat soluția „join fetch” pentru a solicita o entitate cu date dependente într-o singură solicitare, adăugând o alăturare suplimentară la cerere. În CountryDAO , rescrieți interogarea HQL în metoda getAll()la:

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

Lansa. Ne uităm la jurnal, numărăm cererile:

  • 1 - toate țările cu limbi
  • 1 - numărul de orașe
  • 9 - liste de orașe.

Total 11- am reușit)) Dacă nu numai că ați citit tot acest text, ci ați încercat și să îl rulați după fiecare pas de reglare a aplicației, ar trebui să observați chiar și vizual accelerarea întregii aplicații de mai multe ori.

Scrieți o metodă de transformare a datelor

Să creăm un pachet com.codegym.redisîn care adăugăm 2 clase: CityCountry (date despre oraș și țara în care se află acest oraș) și Language (date despre limbă). Iată toate câmpurile care sunt adesea solicitate „după sarcină” în „cererea de frânare”.

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
}

În metoda principală, după ce obțineți toate orașele, adăugați linia

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

Și implementați această metodă:

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

Cred că această metodă se explică de la sine: creăm doar o entitate CityCountry și o completăm cu date din City , Country , CountryLanguage .

Rulați serverul Redis ca container docker

Există 2 opțiuni aici. Dacă faceți pasul opțional „instalați redis-insight, uitați-vă la datele stocate în Redis”, atunci comanda este pentru dvs.:

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

Dacă decideți să săriți peste acest pas, atunci doar:

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

Diferența este că, în prima opțiune, portul 8001 este redirecționat către mașina locală, la care vă puteți conecta cu un client extern pentru a vedea ce este stocat în interior. Și obișnuiam să dau nume semnificative, prin urmare, redis-stacksau redis.

După lansare, puteți vedea lista de containere care rulează. Pentru a face acest lucru, executați comanda:

docker container ls 

Și vei vedea ceva de genul:

Dacă trebuie să găsiți o comandă, puteți fie să vă uitați la ajutorul din terminal (docker help) sau să căutați pe Google „cum să...” (de exemplu, docker cum să eliminați containerul care rulează).

Și am numit și inițializarea clientului ridiche în constructorul principal, dar nu am implementat metoda în sine. Adăugați implementare:

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 a fost adaugat in scop educativ astfel incat in jurnalul de lansare sa vedeti ca totul este OK si conexiunea prin clientul ridiche a trecut fara erori.

Scrieți date în Redis

Adăugați un apel la metoda principală

main.pushToRedis(preparedData); 

Cu implementarea acestei metode:

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

    }
}

Aici, o conexiune sincronă este deschisă cu clientul ridiche și secvenţial fiecare obiect de tip CityCountry este scris în ridiche. Deoarece ridichea este un magazin String cheie-valoare , cheia (id-ul orașului) este convertită într-un șir. Și valoarea este și pentru șir, dar folosind ObjectMapper în format JSON.

Rămâne să rulați și să verificați dacă nu există erori în jurnal. Totul a funcționat.

Instalați redis-insight, uitați-vă la datele stocate în Redis (opțional)

Descărcați redis-insight de pe link și instalați-l. După pornire, arată imediat instanța noastră de ridiche în containerul docker:

Dacă vă conectați, vom vedea o listă cu toate cheile:

Și puteți merge la orice cheie pentru a vedea ce date sunt stocate pe ea:

Scrieți o metodă pentru obținerea datelor de la Redis

Pentru testare, folosim următorul test: obținem 10 înregistrări CityCountry. Fiecare cu o cerere separată, dar într-o singură legătură.

Datele de la ridiche pot fi obținute prin intermediul clientului nostru de ridiche. Pentru a face acest lucru, să scriem o metodă care necesită o listă de id-uri pentru a obține.

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

Implementarea, cred, este intuitivă: deschidem o conexiune sincronă, iar pentru fiecare id obținem un String JSON , pe care îl convertim în obiectul de tipul CityCountry de care avem nevoie .

Scrieți o metodă pentru a obține date din MySQL

În clasa CityDAO , adăugați o metodă getById(Integer id)în care vom obține orașul împreună cu țara:

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

Prin analogie cu paragraful anterior, să adăugăm o metodă similară pentru MySQL la clasa 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();
    }
}

Dintre caracteristici, pentru a fi siguri că obțineți obiectul complet (fără proxy stubs), solicităm în mod explicit o listă de limbi din țară.

Comparați viteza de obținere a acelorași date de la MySQL și Redis

Aici voi da imediat codul metodei principale și rezultatul care se obține pe computerul meu 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();
}

La testare, există o caracteristică - datele din MySQL sunt doar citite, deci nu pot fi repornite între lansările aplicației noastre. Și în Redis sunt scrise.

Deși atunci când încercați să adăugați un duplicat pentru aceeași cheie, datele vor fi pur și simplu actualizate, v-aș recomanda să executați comenzile pentru a opri containerul docker stop redis-stackși a șterge containerul între lansările aplicației în terminal docker rm redis-stack. După aceea, ridicați din nou recipientul cu ridiche docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latestși abia după aceea executați aplicația noastră.

Iată rezultatele testelor mele:

În total, am realizat o creștere a performanței răspunsului la solicitarea „frânare frecventă” de o dată și jumătate. Și asta ținând cont de faptul că în testare nu am folosit cea mai rapidă deserializare prin ObjectMapper . Dacă îl schimbi în GSON, cel mai probabil, poți „câștiga” puțin mai mult timp.

În acest moment, îmi amintesc o glumă despre un programator și despre timp: citește și gândește-te cum să scrii și să-ți optimizezi codul.

Comentarii
  • Popular
  • Nou
  • Vechi
Trebuie să fii conectat pentru a lăsa un comentariu
Această pagină nu are încă niciun comentariu