JavaRush /Kurslar /All lectures for UZ purposes /SQL, JDBC va Hibernate mavzusida loyiha

SQL, JDBC va Hibernate mavzusida loyiha

All lectures for UZ purposes
Daraja, Dars
Mavjud

Bugun JRU to'rtinchi modul bo'yicha yakuniy loyihani qilamiz. Bu nima bo'ladi? Turli texnologiyalar bilan ishlashni sinab ko'ramiz: MySQL, Hibernate, Redis, Docker. Endi aniqroq o'rtaga tushaylik.

Vazifa: bizda MySQL relatsion BD (mamlakat-shahar, mamlakat bo'yicha til) sxemasi bilan bor. Va shahar bo'yicha tez-tez so'rov bo'ladi, u sekin ishlaydi. Biz yechim topdik – tez-tez so'raladigan barcha ma'lumotlarni Redis (in memory storage key-value turida) ga olish.

Bizga MySQL-da saqlanayotgan barcha ma'lumotlar emas, balki faqat tanlangan maydonlar to'plami kerak bo'ladi. Loyiha tutorial formatida bo'ladi. Ya'ni, bu yerda muammoni ko'taramiz va darhol hal qilamiz.

Shunday qilib, bizga qaysi dasturiy vositalar kerak bo'ladi:

  1. IDEA Ultimate (agar kalit tugagan bo'lsa – Romanga slakda yozing)
  2. Workbench (yoki boshqa har qanday MySQL mijozi)
  3. Docker
  4. redis-insight – ixtiyoriy

Bizning harakatlar rejasi:

  1. Dockerni sozlash (tutorialda buni qilmayman, chunki har bir OS uchun o'ziga xos xususiyatlar bo'ladi va internetda "docker-ni windows-ga qanday o'rnatish mumkin" kabi savollarga ko'plab javoblar mavjud), hammasi ishlayotganini tekshirish.
  2. MySQL serverni docker-konteyner sifatida ishga tushirish.
  3. dumpni yuklash.
  4. Idea'da loyiha yaratish, maven dependencies qo'shish.
  5. Domain qatlamini yaratish.
  6. MySQL dan barcha ma'lumotlarni olish uchun metod yozish.
  7. Ma'lumotlarni o'zgartirish metodi yozish (Redisga faqat tez-tez so'raladigan ma'lumotlar yoziladi).
  8. Redis serverni docker-konteyner sifatida ishga tushirish.
  9. Ma'lumotlarni Redisga yozish.
  10. Ixtiyoriy: redis-insightni o'rnatish, Redisda saqlanayotgan ma'lumotlarni ko'rish.
  11. Redisdan ma'lumot olish uchun metod yozish.
  12. MySQLdan ma'lumot olish uchun metod yozish.
  13. MySQL va Redisdan bir xil ma'lumotlarni olish tezligini solishtirish.

Docker'ni sozlash

Docker – dasturlarni ishlab chiqish, yetkazib berish va ishga tushirish uchun ochiq platforma. Biz uni Redisni lokal mashinaga o'rnatmasdan va sozlamasdan foydalanish uchun ishlatamiz, ya'ni tayyor image'dan foydalanamiz. Docker haqida batafsil ma'lumotni bu yerda yoki bu yerda ko'rishingiz mumkin. Agar docker bilan tanish bo'lmasangiz – men ikkinchi havolani ko'rishni tavsiya qilaman.

Docker o'rnatilgan va sozlanganligiga ishonch hosil qilish uchun buyruqni bajaring: docker -v

Agar hammasi OK bo'lsa, docker versiyasini ko'rasiz

MySQL serverni docker-konteyner sifatida ishga tushirish

MySQL va Redisdan ma'lumotlarni etkazish vaqtini solishtirish mumkin bo'lishi uchun MySQLni ham dockerda ishlatamiz. PowerShellda (yoki boshqa konsol terminalda, agar sizda Windows bo'lmasa) buyruqni bajaring:

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

Bu buyruq bilan nima qilayotganimizni ko'rib chiqaylik:

  • docker run – image'ni ishga tushirish (va yuklab olish, agar u hali mahalliy mashinaga yuklab olinmagan bo'lsa). Natijada konteyner ishga tushirilgan bo'ladi.
  • --name mysql – mysql konteynerining nomini belgilaydi.
  • -d – bayroq, bu bayroq konteynerni ishga tushirish terminal oynasini yopilsa ham ishlashda davom etishi kerakligini bildiradi.
  • -p 3306:3306 – portlarni belgilaydi. Ikkita nuqta oldidan – mahalliy mashinada port, ikkita nuqtadan keyin – konteynerdagi port.
  • -e MYSQL_ROOT_PASSWORD=root – MYSQL_ROOT_PASSWORD nomli muhit o'zgaruvchisini root qiymati bilan konteynerga uzatish. Bu flag aynan mysql image'siga xosdir.
  • --restart unless-stopped – konteynerning xatti-harakatlar siyosatini o'rnatishni belgilaydi (konteynerni to'xtatish bo'lsa qayta ishga tushiriladimi?). unless-stopped qiymati to'xtatilgan holatlar bundan mustasno doim qayta ishga tushirish kerakligini bildiradi.
  • -v mysql:/var/lib/mysql – volume (informatsiyani saqlash image'ini) qo'shish.
  • mysql:8 – image nomi va uning versiyasi.

Buyruq bajarilgandan so'ng, terminalda docker image barcha qatlamlarini yuklaydi va konteynerni ishga tushiradi:

Muhim eslatma: agar sizning lokal kompyuteringizda MySQL xizmat sifatida o'rnatilgan va u ishlayotgan bo'lsa – ishga tushirish buyruqida boshqa portni ko'rsatish yoki ishlayotgan xizmatni to'xtatish kerak bo'ladi.

Dumpni yuklash

Dumpni yuklash uchun Workbenchda yangi connection yaratish va parametrlarni ko'rsatish kerak. Men standart portdan (3306) foydalandim, foydalanuvchi nomini o'zgartirmadim (standart bo'yicha root) va root foydalanuvchi uchun parolni belgiladim (root).

Workbench-da Data Import/Restore ni qilamiz va Import from Self Contained File ni tanlaymiz. Fayl sifatida sen dump ni yuklab olgan joyni ko'rsat. Sxemani oldindan yaratishga hojat yo'q – uni yaratish dump faylga kiritilgan. Muvaffaqiyatli importdan so'ng, sizda uchta jadval bilan world sxemasi bo'ladi:

  1. city – bu shahar jadvallari.
  2. country – mamlakatlar jadvallari.
  3. country_language – mamlakatda aholi necha foizi ma'lum bir tilni gapirayotganini ko'rsatadigan jadval.

Biz konteynerni ishga tushirishda volume dan foydalanamiz, shuning uchun mysql konteynerini to'xtatish va hatto o'chirish va ishga tushirishni qayta bajarish (docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --restart unless-stopped -v mysql:/var/lib/mysql mysql:8) dan keyin dumpni yana yuklash kerak emas bo'ladi – u allaqachon volumeda yuklangan to'laqonli.

Idea'da loyiha yaratish, maven dependencies qo'shish

Idea'da loyiha yaratishni allaqachon bilasiz – bu bugungi loyihada eng oson qism.

Pom fayliga zavisimostlarni qo'shamiz:


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

Dastlabki uchta bog'liqlik allaqachon sizga yaxshi tanish.

lettuce-core – bu Redis bilan ishlash uchun mavjud Java klientlaridan biri.

jackson-databind – bu ObjectMapper (Redisda saqlash uchun ObjectMapperdan foydalanish uchun bog'liqlik (String turidagi key-value)) uchun bog'liq.

Shuningdek, resurslar papkasiga (src/main/resources) spy.properties faylini kiriting, unda Hibernate tomonidan bajariladigan parametrlar bilan so'rovlarni ko'rish uchun. Fayl mazmuni:

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

Domain qatlamini yaratish

com.javarush.domain paketini yarating

Menga Idea'da jadvalni strukturasiga mos keladigan entitilar yaratishda qulay bo'lganligi uchun biz BDni Idea'ga qo'shishimiz mumkin.

Men entitilarni quyidagi tartibda yaratishni tavsiya qilaman:

  • Country
  • City
  • CountryLanguage

Mappingni o'zing mustaqil bajarishingiz uchun tavsiya etiladi.

Country klassining kodi:


package com.javarush.domain; 
 
import jakarta.persistence.*; 
 
import java.math.BigDecimal; 
import java.util.Set; 
 
@Entity 
@Table(schema = "world", name = "country") 
public class Country { 
    @Id 
    @Column(name = "id") 
    private Integer id; 
 
    private String code; 
 
    @Column(name = "code_2") 
    private String alternativeCode; 
 
    private String name; 
 
    @Column(name = "continent") 
    @Enumerated(EnumType.ORDINAL) 
    private Continent continent; 
 
    private String region; 
 
    @Column(name = "surface_area") 
    private BigDecimal surfaceArea; 
 
    @Column(name = "indep_year") 
    private Short independenceYear; 
 
    private Integer population; 
 
    @Column(name = "life_expectancy") 
    private BigDecimal lifeExpectancy; 
 
    @Column(name = "gnp") 
    private BigDecimal GNP; 
 
    @Column(name = "gnpo_id") 
    private BigDecimal GNPOId; 
 
    @Column(name = "local_name") 
    private String localName; 
 
    @Column(name = "government_form") 
    private String governmentForm; 
 
    @Column(name = "head_of_state") 
    private String headOfState; 
 
    @OneToOne 
    @JoinColumn(name = "capital") 
    private City city; 
 
    @OneToMany(fetch = FetchType.EAGER) 
    @JoinColumn(name = "country_id") 
    private Set<CountryLanguage> languages; 

 
    //Getters and Setters omitted 

} 

Kodda 3 ta qiziqarli nuqta mavjud.

Birinchisi - bu Continent enum, u BDda ordinal qiymat sifatida saqlanadi. Mamlakat jadvallari tuzilmasida continent maydoniga mos keladigan raqamlarga ko'rish mumkin.


package com.javarush.domain; 
 
public enum Continent { 
    ASIA, 
    EUROPE, 
    NORTH_AMERICA, 
    AFRICA, 
    OCEANIA, 
    ANTARCTICA, 
    SOUTH_AMERICA 
} 

Ikkinchi nuqta – CountryLanguage entitilar to'plami. Bu yerda @OneToMany aloqasi, ikkinchi loyiha uchun modulda yo'q edi. Hibernate standart bo'yicha ushbu to'plamni mamlakat entitasini so'rovlashda yuklamaydi. Ammo, biz barcha qiymatlarni relatsion BDdan cache qilish uchun olishimiz zarur, shuning uchun bu yerda FetchType.EAGER parametri qo'shilgan.

Uchinchisi – city maydoni. Aloqa @OneToOne – xuddi hammasi odatdagiday va tushunarli. Ammo, agar BDdagi foreign key strukturasi qarasangiz, mamlakat (country) poytaxtiga (city) bog'lanadi va shahar (city) mamlakatga (country) bog'lanadi. Aftidan, siklik bog'langan.

Hozircha bu bilan hech narsa qilmasligimiz kerak, ammo "MySQL'dan barcha ma'lumotlarni olish uchun metod yozish" qismiga yetganimizda, Hibernate qanday so'rovlar kiritilayotganini ko'ramiz, ularning sonini ko'ramiz va bu qismni eslaymiz.

City klassining kodi:


package com.javarush.domain; 
 
import jakarta.persistence.*; 
 
@Entity 
@Table(schema = "world", name = "city") 
public class City { 
    @Id 
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
    private Integer id; 
 
    private String name; 
 
    @ManyToOne 
    @JoinColumn(name = "country_id") 
    private Country country; 
 
    private String district; 
 
    private Integer population; 


    //Getters and Setters omitted 

} 

CountryLanguage klassining kodi:


package com.javarush.domain; 
 
import jakarta.persistence.*; 
import org.hibernate.annotations.Type; 
 
import java.math.BigDecimal; 
 
@Entity 
@Table(schema = "world", name = "country_language") 
public class CountryLanguage { 
    @Id 
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
    @Column(name = "id") 
    private Integer id; 
 
    @ManyToOne 
    @JoinColumn(name = "country_id") 
    private Country country; 
 
    private String language; 
 
    @Column(name = "is_official", columnDefinition = "BIT") 
    @Type(type = "org.hibernate.type.NumericBooleanType") 
    private Boolean isOfficial; 
 
    private BigDecimal percentage; 

 
    //Getters and Setters omitted 
} 

MySQL'dan barcha ma'lumotlarni olish uchun metod yozish

Main klassida maydonlarni e'lon qilamiz:


private final SessionFactory sessionFactory; 
private final RedisClient redisClient; 
 
private final ObjectMapper mapper; 
 
private final CityDAO cityDAO; 
private final CountryDAO countryDAO; 

va Main klassining konstruktorida ularni tashqi ishga tushiramiz:


public Main() { 
    sessionFactory = prepareRelationalDb(); 
    cityDAO = new CityDAO(sessionFactory); 
    countryDAO = new CountryDAO(sessionFactory); 
 
    redisClient = prepareRedisClient(); 
    mapper = new ObjectMapper(); 
} 

Ko'rib turganingizdek, metodlar va klasslar etishmayapti - ularni yozaylik.

com.javarush.dao paketini e'lon qiling va unga 2 ta klass qo'shing:


package com.javarush.dao; 
 
import com.javarush.domain.Country; 
import org.hibernate.SessionFactory; 
import org.hibernate.query.Query; 
 
import java.util.List; 
 
public class CountryDAO { 
    private final SessionFactory sessionFactory; 
 
    public CountryDAO(SessionFactory sessionFactory) { 
        this.sessionFactory = sessionFactory; 
    } 
 
    public List<Country> getAll() { 
        Query<Country> query = sessionFactory.getCurrentSession().createQuery("select c from Country c", Country.class); 
        return query.list(); 
    } 
} 

package com.javarush.dao; 
 
import com.javarush.domain.City; 
import org.hibernate.SessionFactory; 
import org.hibernate.query.Query; 
 
import java.util.List; 
 
public class CityDAO { 
    private final SessionFactory sessionFactory; 
 
    public CityDAO(SessionFactory sessionFactory) { 
        this.sessionFactory = sessionFactory; 
    } 
 
    public List<City> getItems(int offset, int limit) { 
        Query<City> query = sessionFactory.getCurrentSession().createQuery("select c from City c", City.class); 
        query.setFirstResult(offset); 
        query.setMaxResults(limit); 
        return query.list(); 
    } 
 
    public int getTotalCount() { 
        Query<Long> query = sessionFactory.getCurrentSession().createQuery("select count(c) from City c", Long.class); 
        return Math.toIntExact(query.uniqueResult()); 
    } 
} 

Endi Main'da bu 2 ta klassni import qilishimiz mumkin. Hala yetishmayotgan ikki metod:


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

Biz hali Redisga yetib bormadik, shuning uchun Redis klientini tashqi ishga tushirish nazorat toshigi holda qoladi:


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

Nihoyat barcha shaharlarni chiqarish metodini yozish mumkin:


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

Realizatsiyaning alohida xususiyati shundan iboratki, biz har safar 500 ta shaharni olamiz. Bu, uzatilgan ma'lumotlar hajmi cheklovlari borligi uchun zarur. Ha, bizning bd'da atigi 4079 ta shahar borligi uchun bunga duch kelmaymiz. Ammo, katta ma'lumotlarni olish kerak bo'lganda, bu usuldan ko'pincha foydalaniladi.

Va main metodi realizatsiyasi:


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

Endi birinchi marta bizning dasturni debug rejimda ishgacha bilish imkoniyatiga egamiz (yoki ishlamaydi – ha, ko'pchilikda shunday bo'ladi).

Shaharlar chiqarilyapti. Agar u avvalgi shaharga BDdan chiqarilmagan bo'lsa, har bir shahar uchun mamlakat chiqariladi. Keling, Hibernate BDya yana qancha so'rovlar yuborishini taxminiy hisoblab chiqaylik:

  • Umumiy shaharlar sonini bilish uchun 1 so'rov (500 ta shaharlar bilan aylanish silsilasi uchun kerak, qachon to'xtash kerakligini bilish uchun).
  • 4079 / 500 = 9 so'rovlar (shaharlar ro'yxati).
  • Har bir shahar uchun agar u avvalgi shaharga BDdan chiqarilmagan bo'lsa, mamlakat chiqariladi. BDda 239 ta mamlakat bo'lganligi sababli, bu bizga 239 ta so'rovni beradi.

Jami 249 so'rovlar. Va bu hali mamlakat bilan birga tillar to'plamini olishni aytgandan so'ng, umuman qorong'u bo'lmasdi. Ammo bu hali juda ko'p, shuning uchun bizning xatti-harakatlarimizni biroz tahlil qilaylik. Nimani qilish kerak, qayerga borish kerak? Agar jiddiy bo'lsa – nima uchun juda ko'p so'rovlar. Agar so'rovlar jurnaliga qarasangiz, har bir mamlakat alohida-alohida so'raladi, shuning uchun birinchi oddiy yechim: ularni birgalikda so'ralganligini aytishimiz kerak, chunki oldindan bilganimizdek, ularni olishimiz kerak bo'lganligi sababli bularning hammasi kode ishlatiladi.

FetchData() metodida boshida tranzaksiyadan keyin to'g'ri oladigan satr qo'sh:

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

So'rovlarni hisoblaymiz:

  • 1 – barcha mamlakatlarni olish
  • 239 – har bir mamlakatni poytaxtini so'rov bo'yicha
  • 1 – shaharlar sonini so'rov qilish
  • 9 – shaharlar ro'yxatini so'rov qilish

Jami 250. Idea yaxshi, ammo ishlamadi. Muammo shundaki, mamlakatda poytaxti (shahar) bilan aloqasi @OneToOne bor. Va bunday aloqa odatga muvofiq darhol yuklanadi (FetchType.EAGER). Biz FetchType.LAZY ni o'rnatib qo'yamiz, chunki biz shunday qilyapmiz, shunday qilib, barcha shaharlarni bir transactionda yuklashimiz mumkin.


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

Poytaxtlar alohida so'ralmay boshladi, ammo so'rovlar soni o'zgarmadi. Endi har bir mamlakat uchun alohida so'rov orqali CountryLanguage ro'yxati olinadi. Ya'ni, bor, va biz to'g'ri yo'nalishda harakat qilyapmiz. Agar eslasangiz, ma'ruzalar taklif qilingan yechimi "join fetch" edi, bu orqali so'rovni qo'shimcha ulanish orqali so'raladigan birlik bilan birga so'raladi. CountryDAO ichida getAll() metodidagi HQL so'rovini qayta yozing:

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

Ishga tushirish. Logni ko'ramiz, so'rovlarni hisoblaymiz:

  • 1 – tillar bilan barcha mamlakatlarni olish
  • 1 – shaharlar sonini olish
  • 9 – shaharlar ro'yxatini olish.

Jami 11 - bizning natijamiz))) Agar siz ushbu barcha matnni birma-bir o'qishingizdan tashqari har bir tahlil qilish qadamidan so'ng dasturni ishga tushirishni sinab ko'rgan bo'lsangiz, siz butun dasturni tayyor qilishning tezligidagi aniqlikni bir necha marta ko'rgansiz.

Ma'lumotlarni o'zgartirish metodini yozish

com.javarush.redis paketini yarating va unga 2 ta klass qo'shing: CityCountry (shahar va uning joylashgan davlat ma'lumotlari) va Language (til bo'yicha ma'lumotlari). Bu yerga "vazifa bo'yicha" ko'pincha so'raladigan ma'lumotlar surilgan.


package com.javarush.redis; 
 
import com.javarush.domain.Continent; 
 
import java.math.BigDecimal; 
import java.util.Set; 
 
public class CityCountry { 
    private Integer id; 
 
    private String name; 
 
    private String district; 
 
    private Integer population; 
 
    private String countryCode; 
 
    private String alternativeCountryCode; 
 
    private String countryName; 
 
    private Continent continent; 
 
    private String countryRegion; 
 
    private BigDecimal countrySurfaceArea; 
 
    private Integer countryPopulation; 
 
    private Set<Language> languages; 
 
    //Getters and Setters omitted 
}

package com.javarush.redis; 
 
import java.math.BigDecimal; 
 
public class Language { 
    private String language; 
    private Boolean isOfficial; 
    private BigDecimal percentage; 
 
    //Getters and Setters omitted 
} 

Main metodida barcha shaharlardan keyin satr qo'shing

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

Va ushbu metodni amalga oshiring:


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

Ushbu metod menimcha hech qanday izohga muhtoj emas: CityCountry ob'ektini yaratib, City, Country, CountryLanguage'dan olingan ma'lumotlarni kiritib beramiz.

Redis serveri docker-konteyner sifatida ishga tushirish

Bu erda 2 ta variant mavjud. Agar redis-insightni o'rnatish, Redis ichida saqlanayotgan ma'lumotlarni ko'rishni qilib ko'rmoqchi bo'lsangiz, siz uchun buyruq:

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

Agar ushbu qadamni o'tkazib yuborishni xohlasangiz, shunda yetarli:

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

Farqi shundaki, birinchi variantda 8001 porti lokal mashinaga o'tkaziladi, unga tashqi mijoz ulanishi mumkin, ichida nima saqlanganini ko'rish mumkin. Va men har doim ahamiyatli nomlarni berishga odatlanganim uchun redis-stack yoki redis.

Ishga tushirgandan so'ng, ishga tushirilgan konteynerlar ro'yxatini ko'rishingiz mumkin. Buning uchun buyruqni bajaring:

docker container ls 

Va quyidagi kabi bir narsa ko'rsangiz:

Agar biron bir buyruqni topishingiz kerak bo'lsa, terminalda helpni ko'rishingiz mumkin (docker help) yoki "how to ..." (masalan, docker how to remove running container) ni googlingiz mumkin.

Bundan tashqari, Mainning konstruktorida redis klientini tashqi ishga tushirishni qo'shgan edik, ammo metod o'zini amalga oshirmadik. Amalga oshirishni qo'shing:


private RedisClient prepareRedisClient() { 
    RedisClient redisClient = RedisClient.create(RedisURI.create("localhost", 6379)); 
    try (StatefulRedisConnection<String, String> connection = redisClient.connect()) { 
        System.out.println("\nRedis ga ulanish o'rnatildi\n"); 
    } 
    return redisClient; 
} 

sout akademik maqsadlar uchun logda ulanish allaqachon e'tishganligini ko'rish uchun qo'shilgan

Ma'lumotlarni Redisga yozish

Main metodida chaqiruv qo'shing

main.pushToRedis(preparedData); 

Ushbu realizatsiyali metod bilan:


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

Bu yerda, biz Redis klienti bilan sinxron ulanishni ochamiz va har bir CityCountry ob'ektini ketma-ket ravishda Redisga yozamiz. Redis bu String turi key-value saqlash turi bo'lgani uchun, kalit (shahar id) satrga o'zgartiriladi. Va qiymat – ObjectMapper orqali JSON formatda satrga.

Endi ishga tushirib ko'rib, logda xatoliklar yo'qligini tekshirish kerak. Hammasi ishladi.

Redis-insightni o'rnatish, Redisda saqlanayotgan ma'lumotlarni ko'rish (ixtiyoriy)

Redis-insightni havola orqali yuklab olish va uni o'rnatish. Ishga tushirgandan so'ng, mendan darhol Redis instantsi docker konteynerida mavjud bo'ldi:

Agar kirilsa – barcha kalitlar ro'yxati ko'rinadi:

Va har qanday kalitga kirib, u bo'yicha saqlanayotgan ma'lumotlarni ko'rish mumkin:

Redisdan ma'lumot olish uchun metod yozish

Ushbu testni sinab ko'rish uchun 10 ta CityCountry yozuvlarini olamiz. Har biri alohida so'rov bilan, ammo bir ulanishda.

Redisda ma'lumotlarni Redis klientimiz orqali olish mumkin. Buning uchun biz kerakli ma'lumotlarni olish uchun id ro'yxatini qabul qiladigan metod yozamiz.


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

Realizatsiya, menimcha, intuitiv tushunarli: sinxron ulanish ochiladi va har bir id uchun JSON String olinadi, undan CityCountry turidagi kerakli ob'ekt yaratiladi.

MySQLdan ma'lumot olish uchun metod yozish

CityDAO klassida getById(Integer id) metodini qo'shish, bu yerda mamlakat bilan birga shahar olamiz:


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

Oldingi punktdagi kabi Main klassida MySQL uchun o'xshash metod qo'shing:


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

Xususiyatlaridan biri, to'liq ob'ektni olish uchun aniqroq (proxy-stubsiz), til ro'yxatini mamlakatdan so'rashni hamma uchungina amalga oshirish kerak bo'ladi.

MySQL va Redisni bir xil ma'lumot olish tezligini solishtirish

Bu yerda main metodini tartibga solib amalga oshiraman va local kompyuterimda qanday natijalar olinayotganini ko'rsataman.


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); 
 
    //hozirgi sessiyani yopamiz, to'liq ma'lumotni aniq olish uchun, kashdan emas 
    main.sessionFactory.getCurrentSession().close(); 
 
    //tasodifiy 10 ta shahar id tanlang 
    //biz novalid holatlarni ko'rib chiqmadi, shuning uchun BDda mavjud bo'lgan id dan foydalaning 
    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(); 
} 

Testlashda bir o'ziga xos holat – MySQLdan ma'lumotlar faqat o'qiladi, shunday qilib, uni dasturning turli ochilishi orasida qayta ishga tushirish shart emas. Ammo Redisda yoziladi.

Agar takrorlangan kalit bo'yicha takroriy urinishda, ma'lumotlar shunchaki yangilanadi deb taxmin qilsak ham, men dasturni ishga tushirish orasida terminalda konteynerni to'xtatish buyruqlarini bajarishni tavsiya qilaman docker stop redis-stack va konteynerni o'chirish docker rm redis-stack. Shundan so'ng, redis konteynerini qayta ishga tushiring docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest va faqat undan keyin bizning dasturimizni ishga tushing.

Bu mening test natijalarim:

Natijada, biz "tormozli tez-tez" so'rovga ishlov berish samaradorligini taxminan bir yarim baravar oshirishga muvaffaq bo'ldik. Va bu ham ObjectMapper orqali eng tez bo'lmagan deserializatsiyaviy to'plamdan foydalanib bo'lgan holda. Agar uni GSON bilan almashtirsak, ehtimol, yana ozgina vaqtni "yutish" mumkin.

Bu vaqtda menga dasturchi va vaqt haqidagi hazil esiga tushadi: o'qib chiqing va kodingizni qanday yozish va optimallashtirish kerakligini o'ylab ko'ring.


Qo'shimcha materiallar

Virtualizatsiya va Docker

Docker Compose

Izohlar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION